package bancosys.tec.persist.hibernate.impl.annotations;

import java.io.Serializable;
import java.sql.Connection;
import java.util.Map;
import java.util.Set;

import javax.naming.NamingException;
import javax.naming.Reference;

import jmine.tec.persist.PersistMessages;

import org.hibernate.FlushMode;
import org.hibernate.Interceptor;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.hibernate.cfg.Configuration;
import org.hibernate.classic.Session;
import org.hibernate.engine.FilterDefinition;
import org.hibernate.impl.FilterImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.metadata.CollectionMetadata;
import org.hibernate.stat.Statistics;

import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.AuthorizationManager;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.hibernate.DirtyAwareSession;
import bancosys.tec.persist.hibernate.PartitionSessionFactory;
import bancosys.tec.persist.hibernate.page.PagingService;

/**
 * Realização base da interface <code>bancosys.tec.persist.PartitionSessionFactory</code>.
 * 
 * @author Alexandre
 */
public class AnnotationPartitionSessionFactory implements PartitionSessionFactory {

    public static final String FILTER_NAME = "authFilter";

    public static final String PARAMETER_NAME = "auth";

    private PagingService pagingService = null;

    private final AuthorizationManager boAuthorizationManager;

    /** Configuração */
    protected Configuration config;

    protected SessionFactory pureSessionFactory;

    /** Guarda partição. */
    protected static final ThreadLocal<AuthorizationContext> tLocal = new ThreadLocal<AuthorizationContext>();

    protected static final ThreadLocal<RequiresNewPartitionInfo> requiresNewLocal = new ThreadLocal<RequiresNewPartitionInfo>();

    protected static final ThreadLocal<Boolean> openPureSessionOnly = new ThreadLocal<Boolean>();

    private final FlushMode flushMode;

    static {
        tLocal.set(AuthorizationContext.hot);
    }

    /**
     * Construtor.
     * 
     * @param config a configuração das propriedades da sessionFactory.
     * @param pureSessionFactory um sessionFactory puro de Hibernate.
     * @param boAuthorizationManager o autorization manager que será utilizado.
     */
    public AnnotationPartitionSessionFactory(Configuration config, SessionFactory pureSessionFactory,
            AuthorizationManager boAuthorizationManager) {
        this.config = config;
        this.pureSessionFactory = pureSessionFactory;
        this.boAuthorizationManager = boAuthorizationManager;
        this.flushMode = FlushMode.AUTO;
    }

    /**
     * Construtor.
     * 
     * @param config a configuração das propriedades da sessionFactory.
     * @param pureSessionFactory um sessionFactory puro de Hibernate.
     * @param boAuthorizationManager o autorization manager que será utilizado.
     * @param flushMode o modo de flush de sessão.
     */
    public AnnotationPartitionSessionFactory(Configuration config, SessionFactory pureSessionFactory,
            AuthorizationManager boAuthorizationManager, FlushMode flushMode) {
        this.config = config;
        this.pureSessionFactory = pureSessionFactory;
        this.boAuthorizationManager = boAuthorizationManager;
        this.flushMode = flushMode;
    }

    /**
     * {@inheritDoc}
     */
    public Session openPureSession(AuthorizationContext partition) {
        return this.getPureSessionFactory(partition).openSession();
    }

    /**
     * {@inheritDoc}
     */
    public SessionFactory getPureSessionFactory(AuthorizationContext partition) {
        return this.pureSessionFactory;
    }

    /**
     * Fecha o sessionFactory.
     */
    public void close() {
        this.getPureSessionFactory(null).close();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        return this.getPureSessionFactory(this.getThreadPartition()).equals(obj);
    }

    /**
     * {@inheritDoc}
     */
    public void evict(Class persistentClass) {
        this.getPureSessionFactory(this.getThreadPartition()).evict(persistentClass);
    }

    /**
     * {@inheritDoc}
     */
    public void evict(Class persistentClass, Serializable id) {
        this.getPureSessionFactory(this.getThreadPartition()).evict(persistentClass, id);
    }

    /**
     * {@inheritDoc}
     */
    public void evictCollection(String roleName) {
        this.getPureSessionFactory(this.getThreadPartition()).evictCollection(roleName);
    }

    /**
     * {@inheritDoc}
     */
    public void evictCollection(String roleName, Serializable id) {
        this.getPureSessionFactory(this.getThreadPartition()).evictCollection(roleName, id);
    }

    /**
     * {@inheritDoc}
     */
    public void evictEntity(String entityName) {
        this.getPureSessionFactory(this.getThreadPartition()).evictEntity(entityName);
    }

    /**
     * {@inheritDoc}
     */
    public void evictEntity(String entityName, Serializable id) {
        this.getPureSessionFactory(this.getThreadPartition()).evictEntity(entityName, id);
    }

    /**
     * {@inheritDoc}
     */
    public void evictQueries() {
        this.getPureSessionFactory(this.getThreadPartition()).evictQueries();
    }

    /**
     * {@inheritDoc}
     */
    public void evictQueries(String cacheRegion) {
        this.getPureSessionFactory(this.getThreadPartition()).evictQueries(cacheRegion);
    }

    /**
     * {@inheritDoc}
     */
    public Map getAllClassMetadata() {
        return this.getPureSessionFactory(this.getThreadPartition()).getAllClassMetadata();
    }

    /**
     * {@inheritDoc}
     */
    public Map getAllCollectionMetadata() {
        return this.getPureSessionFactory(this.getThreadPartition()).getAllCollectionMetadata();
    }

    /**
     * {@inheritDoc}
     */
    public ClassMetadata getClassMetadata(Class persistentClass) {
        return this.getPureSessionFactory(this.getThreadPartition()).getClassMetadata(persistentClass);
    }

    /**
     * {@inheritDoc}
     */
    public ClassMetadata getClassMetadata(String entityName) {
        return this.getPureSessionFactory(this.getThreadPartition()).getClassMetadata(entityName);
    }

    /**
     * {@inheritDoc}
     */
    public CollectionMetadata getCollectionMetadata(String roleName) {
        return this.getPureSessionFactory(this.getThreadPartition()).getCollectionMetadata(roleName);
    }

    /**
     * {@inheritDoc}
     */
    public org.hibernate.classic.Session getCurrentSession() {
        return this.getPureSessionFactory(this.getThreadPartition()).getCurrentSession();
    }

    /**
     * {@inheritDoc}
     */
    public Reference getReference() throws NamingException {
        return this.getPureSessionFactory(this.getThreadPartition()).getReference();
    }

    /**
     * {@inheritDoc}
     */
    public Statistics getStatistics() {
        return this.getPureSessionFactory(this.getThreadPartition()).getStatistics();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return this.getPureSessionFactory(this.getThreadPartition()).hashCode();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isClosed() {
        return this.getPureSessionFactory(this.getThreadPartition()).isClosed();
    }

    /**
     * {@inheritDoc}
     */
    public org.hibernate.classic.Session openSession() {
        return this.openSession(this.getThreadPartition());
    }

    /**
     * {@inheritDoc}
     */
    public org.hibernate.classic.Session openSession(Connection connection) {
        return this.openSession(this.getThreadPartition(), connection);
    }

    /**
     * {@inheritDoc}
     */
    public org.hibernate.classic.Session openSession(Connection connection, Interceptor interceptor) {
        return this.openSession(this.getThreadPartition(), connection, interceptor);
    }

    /**
     * {@inheritDoc}
     */
    public org.hibernate.classic.Session openSession(Interceptor interceptor) {
        return this.openSession(this.getThreadPartition(), interceptor);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.getPureSessionFactory(this.getThreadPartition()).toString();
    }

    /**
     * {@inheritDoc}
     */
    public Configuration getConfiguration() {
        return this.config;
    }

    /**
     * {@inheritDoc}
     */
    public Session openSession(AuthorizationContext partition, Connection conn, Interceptor interceptor) {
        if (this.isThreadPureSessionOnly()) {
            return this.openPureSession(partition);
        }
        SessionFactory factory = this.getPureSessionFactory(partition);
        Session session = factory.openSession(conn, interceptor);
        session.setFlushMode(this.flushMode);

        this.changeSessionAuthorizationContext(session, partition);

        return new DirtyAwareSession(session);
    }

    /**
     * {@inheritDoc}
     */
    public Session openSession(AuthorizationContext partition, Interceptor interceptor) {
        if (this.isThreadPureSessionOnly()) {
            return this.openPureSession(partition);
        }
        SessionFactory factory = this.getPureSessionFactory(partition);
        Session session = factory.openSession(interceptor);
        session.setFlushMode(this.flushMode);

        this.changeSessionAuthorizationContext(session, partition);

        return new DirtyAwareSession(session);
    }

    /**
     * {@inheritDoc}
     */
    public Session openSession(AuthorizationContext partition) {
        if (this.isThreadPureSessionOnly()) {
            return this.openPureSession(partition);
        }
        SessionFactory factory = this.getPureSessionFactory(partition);
        Session session = factory.openSession();
        session.setFlushMode(this.flushMode);

        this.changeSessionAuthorizationContext(session, partition);

        return new DirtyAwareSession(session);
    }

    /**
     * {@inheritDoc}
     */
    public Session openSession(AuthorizationContext partition, Connection conn) {
        if (this.isThreadPureSessionOnly()) {
            return this.openPureSession(partition);
        }
        SessionFactory factory = this.getPureSessionFactory(partition);
        Session session = factory.openSession(conn);
        session.setFlushMode(this.flushMode);

        this.changeSessionAuthorizationContext(session, partition);

        return new DirtyAwareSession(session);
    }

    /**
     * Troca o contexto de autorização de uma sessão.
     * 
     * @param session sessão.
     * @param context contexto de autorização.
     * @return devolve a partição anterior.
     */
    public AuthorizationContext changeSessionAuthorizationContext(org.hibernate.Session session, AuthorizationContext context) {
        AuthorizationContext oldContext = this.getThreadPartition();
        FilterImpl filter = (FilterImpl) session.enableFilter(AnnotationPartitionSessionFactory.FILTER_NAME);
        if (filter == null) {
            throw new PersistenceException(PersistMessages.HOT_COLD_FILTER_NOT_FOUND.create());
        }
        filter.setParameter(AnnotationPartitionSessionFactory.PARAMETER_NAME, this.isAuthPartition(context));
        this.setThreadPartition(context);

        return oldContext;
    }

    /**
     * Se o contexto está em autorização ou não.
     * 
     * @param context o contexto de autorização.
     * @return <code>true</code> se estiver em autorização, <code>false</code> caso contrário.
     */
    private Boolean isAuthPartition(AuthorizationContext context) {
        return AuthorizationContext.cold.equals(context);
    }

    /**
     * {@inheritDoc}
     */
    public AuthorizationContext getThreadPartition() {
        AuthorizationContext requiresNewPartition = this.getRequiresNewThreadPartition();
        return (requiresNewPartition != null ? requiresNewPartition : tLocal.get());
    }

    /**
     * {@inheritDoc}
     */
    public void setThreadPartition(AuthorizationContext partition) {
        tLocal.set(partition);
    }

    /**
     * {@inheritDoc}
     */
    public void setThreadPureSessionOnly(boolean pureSessionOnly) {
        openPureSessionOnly.set(pureSessionOnly);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isThreadPureSessionOnly() {
        Boolean pure = openPureSessionOnly.get();
        if (pure == null) {
            return false;
        }
        return pure.booleanValue();
    }

    /**
     * Devolve o contexto de autorização da thread local.
     * 
     * @return o contexto de autorização da thread local.
     */
    public static AuthorizationContext getPartition() {
        return tLocal.get();
    }

    /**
     * Guarda o contexto de autorização original e o passado como parâmetro na thread local.
     * 
     * @param partition um contexto de autorização a ser guardado.
     */
    public void setRequiresNewThreadPartition(AuthorizationContext partition) {
        RequiresNewPartitionInfo partitionInfo = new RequiresNewPartitionInfo(partition, this.getThreadPartition());
        requiresNewLocal.set(partitionInfo);
    }

    /**
     * Devolve o contexto de autorização guardado na thread local, caso exista, e null caso contrário.
     * 
     * @return o contexto de autorização guardado na thread local, caso exista, e null caso contrário.
     */
    public AuthorizationContext getRequiresNewThreadPartition() {
        RequiresNewPartitionInfo partitionInfo = requiresNewLocal.get();
        return ((partitionInfo != null) ? partitionInfo.getPartition() : null);
    }

    /**
     * {@inheritDoc}
     */
    public void restoreThreadPartition() {
        RequiresNewPartitionInfo partitionInfo = requiresNewLocal.get();
        tLocal.set(partitionInfo.getOriginalPartition());
        requiresNewLocal.set(null);
    }

    /**
     * Guarda o contexto de autorização original e o atual.
     * 
     * @author ?
     */
    public static class RequiresNewPartitionInfo {
        private AuthorizationContext partition = AuthorizationContext.hot;

        private AuthorizationContext originalPartition = null;

        /**
         * Construtor.
         * 
         * @param partition um novo contexto de autorização.
         * @param oldPartition o contexto de autorização original.
         */
        public RequiresNewPartitionInfo(AuthorizationContext partition, AuthorizationContext oldPartition) {
            this.partition = partition;
            this.originalPartition = oldPartition;
        }

        /**
         * Devovle o contexto de autorização original.
         * 
         * @return o contexto de autorização original.
         */
        public AuthorizationContext getOriginalPartition() {
            return this.originalPartition;
        }

        /**
         * Define o contexto de autorização original.
         * 
         * @param originalPartition o contexto de autorização original.
         */
        public void setOriginalPartition(AuthorizationContext originalPartition) {
            this.originalPartition = originalPartition;
        }

        /**
         * Devovle o novo contexto de autorização.
         * 
         * @return o novo contexto de autorização.
         */
        public AuthorizationContext getPartition() {
            return this.partition;
        }

        /**
         * Define o novovcontexto de autorização.
         * 
         * @param partition o novo contexto de autorização.
         */
        public void setPartition(AuthorizationContext partition) {
            this.partition = partition;
        }
    }

    /**
     * {@inheritDoc}
     */
    public StatelessSession openStatelessSession() {
        return this.openStatelessSession(this.getThreadPartition());
    }

    /**
     * {@inheritDoc}
     */
    public StatelessSession openStatelessSession(AuthorizationContext partition) {
        SessionFactory factory = this.getPureSessionFactory(partition);
        return factory.openStatelessSession();
    }

    /**
     * {@inheritDoc}
     */
    public StatelessSession openStatelessSession(Connection conn) {
        return this.openStatelessSession(this.getThreadPartition(), conn);
    }

    /**
     * {@inheritDoc}
     */
    public StatelessSession openStatelessSession(AuthorizationContext partition, Connection conn) {
        SessionFactory factory = this.getPureSessionFactory(partition);
        return factory.openStatelessSession(conn);
    }

    /**
     * {@inheritDoc}
     */
    public Set getDefinedFilterNames() {
        SessionFactory factory = this.getPureSessionFactory(this.getThreadPartition());
        return factory.getDefinedFilterNames();
    }

    /**
     * {@inheritDoc}
     */
    public FilterDefinition getFilterDefinition(String name) {
        SessionFactory factory = this.getPureSessionFactory(this.getThreadPartition());
        return factory.getFilterDefinition(name);
    }

    /**
     * {@inheritDoc}
     */
    public PagingService getPagingService() {
        return this.pagingService;
    }

    /**
     * {@inheritDoc}
     */
    public void setPagingService(PagingService pagingService) {
        this.pagingService = pagingService;
    }

    /**
     * Devolve o BOAuthorizationManager.
     * 
     * @return o BOAuthorizationManager.
     */
    public AuthorizationManager getBoAuthorizationManager() {
        return this.boAuthorizationManager;
    }

    /**
     * Devolve <code>true</code> se o o filtro de autorização estiver ligado para a sessão, <code>false</code> caso contrário.
     * 
     * @param session sessão.
     * @return <code>true</code> se o o filtro de autorização estiver ligado para a sessão, <code>false</code> caso contrário
     */
    public static boolean isAuthorizationFilterEnabled(org.hibernate.Session session) {
        return session.getEnabledFilter(AnnotationPartitionSessionFactory.FILTER_NAME) != null;
    }

    /**
     * Desabilita o filtro de autorização para uma sessão.
     * 
     * @param session sessão.
     */
    public static void disableAuthorizationFilter(org.hibernate.Session session) {
        session.disableFilter(AnnotationPartitionSessionFactory.FILTER_NAME);
    }

    /**
     * Liga o filtro de autorização para um sessão.
     * 
     * @param session sessão.
     */
    public static void enableAuthorizationFilter(org.hibernate.Session session) {
        FilterImpl filter = (FilterImpl) session.enableFilter(AnnotationPartitionSessionFactory.FILTER_NAME);
        if (filter == null) {
            throw new PersistenceException(PersistMessages.HOT_COLD_FILTER_NOT_FOUND.create());
        }
        filter.setParameter(AnnotationPartitionSessionFactory.PARAMETER_NAME, AuthorizationContext.cold.equals(tLocal.get()));
    }
}
